我們在之前Django的章節沒有特別仔細聊過使用視圖類別,有幾個因素:
但是在API方面,我個人反而是比較喜歡使用Class Based View(CBV)大於FBV,有幾個原因:
當然CBV的缺點就是開發會比較笨重,且過度封裝的代價就是debug與自定義邏輯有時候會作繭自縛,所以還是要根據個人的需求進行調整
今日重點如下:
今日的程式碼:https://github.com/class83108/drf_demo/tree/CBV
我們昨天使用APIView
來初步體驗一下Django Rest Framework(DRF)的CBV開發形式,可能會覺得跟FBV也沒什麼區別啊?那就得提到GenericAPIView
了
首先我們看一下APIView的部分,在DetailView中,我們有一個通用的方法是先根據primary key來拿到模型實例,然後再根據請求方法進行對應的操作
class WorkspaceDetail(APIView):
def get_object(self, pk):
try:
return Workspace.objects.get(pk=pk)
except Workspace.DoesNotExist:
raise Response(status=status.HTTP_404_NOT_FOUND)
def get(self, request, pk):
workspace = self.get_object(pk)
serializer = WorkspaceSerializer(workspace)
return Response(serializer.data)
def put(self, request, pk):
workspace = self.get_object(pk)
serializer = WorkspaceSerializer(workspace, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk):
workspace = self.get_object(pk)
workspace.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
那我們接著來看GenericAPIView
class WorkspaceDetail(GenericAPIView):
queryset = Workspace.objects.all()
serializer_class = WorkspaceSerializer
def get(self, request, pk):
workspace = self.get_object()
serializer = self.get_serializer(workspace)
return Response(serializer.data)
def put(self, request, pk):
workspace = self.get_object()
serializer = self.get_serializer(workspace, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk):
workspace = self.get_object()
workspace.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
看到直接定義好queryset
與serializer_class
兩個屬性,能讓人快速知道這個CBV是要使用哪個序列化器來處理資料
這邊可能會有幾個疑問:
queryset = Workspace.objects.all()
self.get_object()
1跟2需要一起回答,首先我們來看get_object的源碼
def get_object(self):
"""
Returns the object the view is displaying.
You may want to override this if you need to provide non-standard
queryset lookups. Eg if objects are referenced using multiple
keyword arguments in the url conf.
"""
queryset = self.filter_queryset(self.get_queryset())
# Perform the lookup filtering.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
assert lookup_url_kwarg in self.kwargs, (
'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' %
(self.__class__.__name__, lookup_url_kwarg)
)
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = get_object_or_404(queryset, **filter_kwargs)
# May raise a permission denied
self.check_object_permissions(self.request, obj)
return obj
self.get_queryset()
拿到queryset屬性定義的查詢集,並且經過self.filter_queryset
後拿到真正查詢時的queryset,self.filter_queryset可以透過以下方式進行定義,不建議直接改寫# settings.py
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [
# 你要進行的filter
...
]
}
self.lookup_url_kwarg
或self.lookup_field
來找到路由中的參數名,通常是主鍵或是idclass GenericAPIView(views.APIView):
lookup_field = 'pk'
lookup_url_kwarg = None
這時我們就可以回答第一個與第二個問題,設定queryset屬性更像是我們告訴這個視圖類要去找哪一個模型查資料,而**self.get_object()**才是真正實現我們要怎麼查詢的最終方法
那我們說說使用GenericAPIView的好處,當我們不需要特別修改get_object()時,可以用不自己寫方法,但是還是保留了我們可以覆寫get_object()的自由度。並且GenericAPIView還能透過屬性方法來定義分頁與分類功能,這也是APIView所沒有的(這部分留到Day20再說明)
Mixin提供了更多有關基本視圖行為的操作,而不是直接定義get或是post方法
例如:
ListModelMixin:返回模型實例的列表響應
RetrieveModelMixin:返回單一模型實例的列表響應
並且也提供了perform_create()
與 perform_update()
方法,讓我們在建立新物件或是更新時,有更靈活的操作
這邊我們就拿ListView來做示範(上一個示範是DetailView)
from rest_framework.mixins import (
ListModelMixin,
CreateModelMixin,
)
class WorkspaceList(GenericAPIView, ListModelMixin, CreateModelMixin):
queryset = Workspace.objects.all()
serializer_class = WorkspaceSerializer
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
可以看到在get方法中,調用了list方法返回列表響應,而在post方法中,則使用create方法來建立物件
list方法我這邊不特別解釋,我們則可以來好好看一下CreateModelMixin
的源碼中是如何定義create方法
class CreateModelMixin:
"""
Create a model instance.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
serializer.save()
也就是說perform_create方法,當你的資料都是要按照request.data中的資料完全不改的儲存,可以不用特別定義,反之我們可以在這方法中定義儲存時的業務邏輯
可以看到perform_create中我們將owner定義為self.request.user,因此我們將資料直接發出POST請求
{
"name": "demo 10", "members": [2]
}
什麼!我們不是定義好owner是誰了啊?為什麼還要說我沒附上資料,我們回去看源碼
必須要先驗證過,才會進到perform_create,而我們的序列化器中,owner不是只讀屬性,所以還是得定義好
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
也因此如果我們定義了perform_create,今天你設定哪一個owner,最後儲存的都還是你在perform_create所設定的,這點需要注意
但是如果有看Django REST framework: 序列化器與視圖函式 開啟API之旅的讀者應該對於剛剛的操作有違和感,沒錯!我們是使用字典而非列表來建立資料,這也是一個我覺得有點可惜的地方
如果想要更符合Restful風格一點的話,除了修改post方法中去判斷是否為list之外,我更偏好直接修改序列化器,讓我們的視圖類別更簡潔
class WorkspaceSerializer(serializers.ModelSerializer):
class Meta:
model = Workspace
fields = [
"id",
"name",
"owner",
"members",
"created_at",
]
read_only_fields = ["id", "created_at"]
def create(self, validated_data):
if isinstance(validated_data, list):
return Workspace.objects.bulk_create(validated_data)
return super().create(validated_data)
使用GenericAPIView搭配Mixin的好處是,Mixin的方法命名更直觀一點,因為GET不會知道是列表還是單一對象,而有時候POST也不一定得創建資料,可能是想要驗證數據,因此搭配Mixin我們更能把通用的GenericAPIView打造成不失通用且靈活的作法
但是同時應該也注意到了,為了通用而做的封裝在我們不了解其中的原理時,在開發上可能會產生不如我們預期的結果
雖然GenericAPIView搭配Mixin已經大幅減少我們的程式碼量,但是還是不免讓人覺得在get方法裡再調用list方法,在post中調用create方法好像沒有那麼直觀,這時候Generics
就登場啦
我們可以直接看程式碼,非常簡潔並且容易理解
從ListCreateAPIView與RetrieveUpdateDestroyAPIView的名稱我們就能明白他的作用
from rest_framework import generics
class WorkspaceList(generics.ListCreateAPIView):
queryset = Workspace.objects.all()
serializer_class = WorkspaceSerializer
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
class WorkspaceDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Workspace.objects.all()
serializer_class = WorkspaceSerializer
def perform_update(self, serializer):
instance = serializer.save()
if "members" in self.request.data:
instance.members.set(self.request.data["members"])
get
和 post
方法get
、 put
、patch
與 delete
方法perform_create
與perform_update
想要看還有提供哪些方法可以看底下的參考資料
如果我們想用通用方法大幅減少我們程式碼量的同時,又想要微調一些特定的方法(通常是queryset),可以參考官方文檔的範例
建立自定義Mixin,並且自定義get_object使其能夠去找url中我們定義在self.lookup_fields的關鍵字參數
class MultipleFieldLookupMixin:
"""
Apply this mixin to any view or viewset to get multiple field filtering
based on a `lookup_fields` attribute, instead of the default single field filtering.
"""
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
filter = {}
for field in self.lookup_fields:
if self.kwargs.get(field): # Ignore empty fields.
filter[field] = self.kwargs[field]
obj = get_object_or_404(queryset, **filter) # Lookup the object
self.check_object_permissions(self.request, obj)
return obj
在視圖類中進行繼承並且設置好屬性就能透過非常精簡的方式快速完成CRUD等相關需求
class RetrieveUserView(MultipleFieldLookupMixin, generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
lookup_fields = ['account', 'username']
在Generics我們已經非常大幅度的簡化程式碼,但是如果我們就只想要一個大而全的對象呢?可以完成對這個模型的所有操作?DRF中的ViewSet就可以幫助我們整合多個視圖類別達到最極致的簡化
ViewSet跟前面Generics比較不同的是ViewSet將常見的 CRUD 操作抽象化為一組標準方法(list, create, retrieve, update, destroy),而比較不是像Generics所進行的封裝:隱藏實現細節
那我們直接來看程式碼吧
from rest_framework import viewsets
class WorkspaceViewSet(viewsets.ModelViewSet):
queryset = Workspace.objects.all()
serializer_class = WorkspaceSerializer
ViewSet中有幾種視圖集:
ModelViewSet
繼承自 GenericAPIView
,並通過混合各種 Mixin 類的行為來包含各種操作的實現,如 list
、 retrieve
、create
等等,較常使用此視圖集list
與 retrieve
也因為現在不會顯示的寫接收的方法接口,因此在路由中我們需要寫DefaultRouter幫忙生成路由
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import (
WorkspaceViewSet,
)
router = DefaultRouter()
router.register(r"workspaces", WorkspaceViewSet)
urlpatterns = [
...
path("", include(router.urls)),
]
那我們看一下要怎麼實際操作吧,我們這邊用postman示範,比較能控制請求方法
注意url中有附上id
APPEND_SLASH
可以在settings.py中修改
那這時候會想:
第一種很單純,就是設置對應的屬性就好
class WorkspaceViewSet(viewsets.ModelViewSet):
# 限制只允許某些 HTTP 方法
http_method_names = ["get"]
queryset = Workspace.objects.all()
serializer_class = WorkspaceSerializer
第二個問題的話,我們可以自訂一個方法是用來返回workerspace下所有Document的名稱
忘記我們模型配置的話:https://github.com/class83108/drf_demo/blob/CBV/drf_demo/note/models.py
class WorkspaceViewSet(viewsets.ModelViewSet):
queryset = Workspace.objects.all()
serializer_class = WorkspaceSerializer
@action(detail=True, methods=["get"])
def document_names(self, request, pk=None):
workspace = self.get_object()
document_names = workspace.documents.values_list("title", flat=True)
return Response({"document_names": list(document_names)})
action裝飾器代表了將其標記成路由,並且有以下功能
permission_classes
, serializer_class
, filter_backends
等來覆蓋視圖集的屬性我們設置好了之後根據路由請求
CBV 類型 | 特點 | 適用場景 | 優點 | 缺點 |
---|---|---|---|---|
APIView | 需手動處理 HTTP 方法 | 1. 需要完全控制請求處理流程 2. 自定義複雜邏輯 | 1. 靈活性最高 2. 可以精確控制每個細節 | 需要編寫最多程式碼 |
GenericAPIView | 提供通用功能如查詢集和序列化器 | 需要一些通用功能但仍需自定義邏輯 | 1. 減少重複代碼 2. 提供常用功能 | 1. 仍需手動實現 CRUD 方法 2. 對於簡單場景可能過於複雜 |
GenericAPIView + Mixins | 1. 將通用操作分解為可重用的組件 2. 可以組合不同的 Mixins | 1. 需要靈活組合不同 CRUD 操作 2. 自定義特定 CRUD 行為 | 高度可組合,可以精確控制包含哪些操作 | 1. 可能需要多重繼承 2. 對新手可能不夠直觀 3. 有Generics 類別與自定義Mixin後存在感很低 |
Generics 類別 | 1. 預先組合了常用的 GenericAPIView 和 Mixins 2. 提供開箱即用的 CRUD 操作 | 1. 標準的 CRUD 操作 2. 快速開發 RESTful API | 1. 程式碼極其簡潔2. 快速實現標準 API | 1. 靈活性較低 2. 自定義行為可能受限 |
ViewSet | 1. 將一組相關視圖邏輯組合在一個類別中 2. 與路由器集成良好 | 1. 需要在一個類中處理資源的所有操作 2. 構建完整的 RESTful API | 1. 程式碼更簡潔 2. 快速實現標準 API 3. 容易擴展 | 1. 可能包含不需要的方法 2. 對於簡單 API 可能過於重量級 3. 多了一層抽象,可讀性較差 |
使用建議:
我們今天對DRF中的CBV做了更完整與更深入的介紹,DRF中的CBV的確種類繁多並且不熟悉中間的實現原理,可能會造成使用上有很大的障礙。如果想要一開始就先體驗CBV的便捷,可以使用Generics 類別然後自定義自己的Mixin來做開發,既可以應付複雜需求,在基礎開箱即用的部分也有很高的覆蓋性